Jump-start your best year yet:

Become a member and get 25% off the first year

How Static Variables Behave Across Multiple Libraries

Yakup Cengiz

7 min readJul 6, 2023

In software development, it is common to divide code into multiple libraries for modularity and reusability. When multiple libraries are used in a program, it is important to understand how static variables behave across those libraries. This article explores the behavior of static variables in such scenarios, focusing on the memory address and value changes when accessing static variables from different libraries.

Static Variables and Libraries

Static variables are a powerful feature in programming languages that allow variables to retain their values throughout the lifetime of a program execution. When using static variables within libraries, there are certain considerations to keep in mind.

Memory Address of Static Variables

When multiple libraries use the same static variable defined in a third library, the memory address of the static variable will be the same within each library. However, it’s worth noting that the memory addresses of the static variable in each library can be different. This is because each library instance maintains its own copy of the static variable.

Let’s consider a simple scenario to illustrate this behavior. Suppose we have three libraries: LibraryA, LibraryB, and LibraryC. Both LibraryA and LibraryB depend on LibraryC, and they use the same static variable defined in LibraryC.

LibraryC.h

#pragma once

#include "LibraryCGlobal.h"

// workaround to generate libraryC.lib file.
LIBC_API int func();

// Static variable declaration
static int STATIC_VAR = 15;

LibraryC.cpp

#include "LibraryC.h"

// to generate libraryC.lib file
int func()
{
return 0;
}

LibraryA.cpp

#include "LibraryA.h"
#include "LibraryC.h"
#include <iostream>

// Function in LibraryA
void libraryAFunction() {
// Update the value of STATIC_VAR
STATIC_VAR = 20;

// Print the memory address and value of STATIC_VAR
std::cout << "Memory address of 'STATIC_VAR' in " << "LibraryA" << ": " << &STATIC_VAR <<", value : "<< STATIC_VAR << std::endl;
}

LibraryB.cpp

#include "LibraryB.h"
#include "LibraryC.h"
#include <iostream>

// Function in LibraryB
void libraryBFunction() {
std::cout << "Memory address of 'STATIC_VAR' in " << "LibraryB" << ": " << &STATIC_VAR <<", value : "<< STATIC_VAR << std::endl;
}

Application main.cpp file


#include <iostream>
#include "LibraryA.h"
#include "LibraryB.h"

int main()
{
// Call functions from LibraryA and LibraryB
libraryAFunction();
libraryBFunction();
}

The output of this program is as follows:

Memory address of 'STATIC_VAR' in LibraryA: 7B1DC000, value : 20
Memory address of 'STATIC_VAR' in LibraryB: 7B20C000, value : 15

This demonstrates that modifying the static variable in one library does not update the value in other libraries. Each library maintains its own separate instance of the static variable.

It’s important to note that if multiple instances of the same library are dynamically linked separately in an application, each instance will have its own copy of the static variable. Therefore, changing the value of the static variable in one library instance will not affect other instances of the library. In C++, variables defined as static within a shared library are not directly accessible outside of the library. The static keyword in this context is used to limit the visibility of the variable to the scope of the translation unit, meaning it can only be accessed within the file where it is defined. A translation unit is a self-contained source file that is compiled into an object file. A static variable is a variable that has internal linkage, meaning that it is only visible to the translation unit in which it is declared. In C++, a static variable declared in a translation unit is created once and shared by all functions in that translation unit. This means that the variable will only have one copy in memory, even if it is used by multiple functions.

Understanding how static variables behave across multiple libraries is crucial for developing robust and reliable software systems. By grasping this behavior, developers can effectively manage shared data across multiple libraries and ensure the desired behavior of static variables in their programs.

The source code for this article can be downloaded from:

Note that the same behavior can be seen in an exe file without using a library. The purpose of this article was to point out a situation that needs to be taken into account when developing a library. I added a new project to the repository to demonstrate this in an exe file without adding libraries. I won’t explain all the details, if you are curious you can access the code here.

Understanding Global Variables and Using extern

Global variables are variables that can be accessed and modified by any part of a program, regardless of their scope. They are declared outside of any function or class, typically at the top of a file, making them accessible throughout the entire program. However, when working with multiple libraries or translation units, using global variables across different files can become challenging due to issues related to variable visibility and linker errors.

One solution to this problem is to use the extern keyword in C++. The extern keyword is used to declare a global variable that is defined in another translation unit or library. It tells the compiler that the actual definition of the variable exists elsewhere and that it should be linked during the compilation process.

Let’s see how extern can be used to share global variables across libraries.

// LibraryC.cpp
#pragma once
#include “LibraryCGlobal.h”
// Static variable declaration
LIBC_API int func();
static int VAR = 15;
int VAR2 = 15;
// both declaration and definition
extern int VAR3 = 15;
extern int VAR4;
LIBC_API extern int VAR5;

LibraryC.cpp is located in a shared library. It declares several global variables: VAR, VAR2, VAR3, VAR4, and VAR5.

The keyword extern is used to declare VAR3, VAR4, and VAR5 as external variables. This means their actual definitions exist in other files, and they will be linked during the compilation process. The extern keyword ensures that the variables’ declarations are visible across multiple translation units.

Now, let’s look at two other files, LibraryA.cpp and LibraryB.cpp, which utilize the global variables declared in LibraryC.cpp:

// LibraryA.cpp
#include "LibraryA.h"
#include "LibraryC.h"
#include <iostream>
int VAR4;
void libraryAFunction() {
VAR = 20;
VAR2 = 20;
VAR3 = 20;
VAR4 = 20;
VAR5 = 20;
std::cout << "Memory address of 'VAR' in " << "LibraryA" << ": " << &VAR << ", value : " << VAR << std::endl;
std::cout << "Memory address of 'VAR2' in " << "LibraryA" << ": " << &VAR2 << ", value : " << VAR2 << std::endl;
std::cout << "Memory address of 'VAR3' in " << "LibraryA" << ": " << &VAR3 << ", value : " << VAR3 << std::endl;
std::cout << "Memory address of 'VAR4' in " << "LibraryA" << ": " << &VAR4 << ", value : " << VAR4 << std::endl;
std::cout << "Memory address of 'VAR5' in " << "LibraryA" << ": " << &VAR5 << ", value : " << VAR5 << std::endl;
std::cout << " - - -" << std::endl;
}

// LibraryB.cpp
#include "LibraryB.h"
#include "LibraryC.h"
#include <iostream>
int VAR4;
void libraryBFunction() {
std::cout << "Memory address of 'VAR' in " << "LibraryB" << ": " << &VAR << ", value : " << VAR << std::endl;
std::cout << "Memory address of 'VAR2' in " << "LibraryB" << ": " << &VAR2 << ", value : " << VAR2 << std::endl;
std::cout << "Memory address of 'VAR3' in " << "LibraryB" << ": " << &VAR3 << ", value : " << VAR3 << std::endl;
std::cout << "Memory address of 'VAR4' in " << "LibraryB" << ": " << &VAR4 << ", value : " << VAR4 << std::endl;
std::cout << "Memory address of 'VAR5' in " << "LibraryB" << ": " << &VAR5 << ", value : " << VAR5 << std::endl;
}

// main.cpp
#include "LibraryA.h"
#include "LibraryB.h"
int main() {
libraryAFunction();
libraryBFunction();
}

In this example, LibraryA.cpp and LibraryB.cpp include the necessary header files (LibraryC.h) to access the global variables declared in LibraryC.cpp. These header files contain the declarations of the global variables using the extern keyword.

When executing the code, we can observe the following:

In LibraryA.cpp, we define an external variable VAR4 and modify the values of global variables VAR, VAR2, VAR3, VAR4, and VAR5. We then print the memory addresses and values of these variables.

In LibraryB.cpp, we include the necessary headers and print the memory addresses and values of the same set of variables. Note that we don’t modify their values in this file.

Finally, in main.cpp, we call the functions libraryAFunction() and libraryBFunction() to execute the code in LibraryA.cpp and LibraryB.cpp, respectively.

Output of running this program:

Memory address of 'VAR' in LibraryA: 00007FFEAFC6D008, value : 20
Memory address of 'VAR2' in LibraryA: 00007FFEAFC6D000, value : 20
Memory address of 'VAR3' in LibraryA: 00007FFEAFC6D004, value : 20
Memory address of 'VAR4' in LibraryA: 00007FFEAFC6D170, value : 20
Memory address of 'VAR5' in LibraryA: 00007FFEAAF1C008, value : 20
- - -
Memory address of 'VAR' in LibraryB: 00007FFEAF6ED008, value : 15
Memory address of 'VAR2' in LibraryB: 00007FFEAF6ED000, value : 15
Memory address of 'VAR3' in LibraryB: 00007FFEAF6ED004, value : 15
Memory address of 'VAR4' in LibraryB: 00007FFEAF6ED170, value : 0
Memory address of 'VAR5' in LibraryB: 00007FFEAAF1C008, value : 20

LIBC_API extern int VAR5 declares the external variable VAR5, indicating that its definition exists somewhere else. The LIBC_API suggests that this variable is part of an API provided by a library. By declaring it as extern, you’re informing the compiler that the actual definition of VAR5 will be available at the linking stage, ensuring that it is resolved correctly. VAR5 is modified in libraryA, and the value is read from the same memory address in libraryB.

This example demonstrates how the extern keyword facilitates sharing global variables across libraries. You can access the source code from the github repository.